跳到主要内容

Backend Developer Roadmap 3

· 阅读需 18 分钟

Roadmap: https://roadmap.sh/backend

本文隶属于 Roadmap 中的 Internet --> Browsers and how they work?

原文:https://developer.mozilla.org/en-US/docs/Web/Performance/How_browsers_work

用户希望网站内容快速加载且交互顺畅,因此开发人员应该努力实现这两个目标。为了理解如何提高性能和感知性能,有必要了解浏览器的工作原理。

总览

快速的网站提供更好的用户体验。用户希望并期望具有快速加载和顺畅交互的内容的 Web 体验。

Web 性能的两个主要问题与延迟有关,并且与浏览器大多数情况下是单线程有关。

延迟是影响页面加载速度的主要因素,开发人员的目标是让站点尽可能快地加载,以便用户尽快获得所需信息。浏览器大多数情况下是单线程的,这意味着它们在占用另一个任务之前从头到尾执行一个任务。为了确保网站流畅互动,开发人员需要确保主线程可以完成所有工作并在可能和适当的情况下最小化主线程的责任,以提高 Web 性能。

导航

导航是加载网页的第一步。它发生在用户通过地址栏输入 URL、点击链接、提交表单以及其他操作请求页面时。Web 性能的目标之一是尽量减少导航完成所需的时间。在理想的情况下,这通常不会花费太长时间,但延迟和带宽可能会导致时延。

DNS 查找

访问网页的第一步是查找页面资源的位置。如果访问https://example.com,那么 HTML 页面位于 IP 地址为93.184.216.34的服务器上。如果你从未访问过此站点,则需要进行 DNS 查找。

浏览器请求 DNS 查找,最终由名称服务器(name server)响应 IP 地址。此后,IP 地址可能会被缓存一段时间,这样就可以通过从缓存中检索IP地址而不是再次联系名字服务器来加速后续请求。

页面加载通常只需要对每个主机名进行一次 DNS 查找。但是,对于所请求页面引用的每个唯一主机名都必须进行 DNS 查找。如果您的字体、图像、脚本、广告和指标都具有不同的主机名,则必须对每个主机名进行 DNS 查找。这可能会影响性能,特别是在移动网络上。

TCP 握手

浏览器通过 TCP 三次握手与服务器建立连接,以便在传输数据之前,协商网络 TCP 套接字连接的参数。

TCP 的三次握手技术通常被称为“SYN-SYN-ACK”或更准确地说是SYN、SYN-ACK 和 ACK,因为 TCP 需要发送三个消息来协商和启动两台计算机之间的 TCP 会话。这意味着每台服务器之间需要三个以上的消息传递,而且此时请求还尚未完成。

TLS 协商

对于建立在 HTTPS 上的安全连接,需要进行另一次“握手”。这个握手,或者更准确地说是 TLS 协商,确定要用于加密通信的密码,验证服务器,并在开始实际数据传输之前建立安全连接。这需要向服务器发送三次往返之前,才会实际发送内容请求。

https://developer.mozilla.org/en-US/docs/Web/Performance/How_browsers_work/ssl.jpg

建立安全连接虽然增加了页面加载时间,但是安全连接的延迟成本是值得的,因为在浏览器和 Web 服务器之间传输的数据无法被第三方解密。

经过 8 次往返后,浏览器终于能够发出请求。

响应

当浏览器与 Web 服务器成功建立连接后,浏览器会代表用户发送初始的 HTTP GET 请求,往往是一个 HTML 文件。Web 服务器接收请求后,会回复相关响应头以及 HTML 的内容。

<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="UTF-8" />
<title>My simple page</title>
<link rel="stylesheet" href="styles.css" />
<script src="myscript.js"></script>
</head>
<body>
<h1 class="heading">My Page</h1>
<p>A paragraph with a <a href="<https://example.com/about>">link</a></p>
<div>
<img src="myimage.jpg" alt="image description" />
</div>
<script src="anotherscript.js"></script>
</body>
</html>

这个初始请求的响应包含了收到的第一个数据字节。Time to First Byte (TTFB) 是用户发出请求(比如点击链接)到收到 HTML 第一个包的时间。第一块内容通常包含 14KB 数据。

在上面的例子中,请求肯定小于 14KB,但链接资源直到浏览器在解析过程中遇到链接才会请求,解析过程在下面描述。

TCP 慢启动 / 14KB 规则

TCP 慢启动是一个算法,它平衡了网络连接的速度。初始响应包的大小为 14KB,这是 TCP 慢启动的一部分,它逐渐建立适合网络速度的传输速度,以避免拥塞。当接收到初始数据包时,服务器会将下一个数据包的大小加倍到约 28KB,随后的数据包会逐渐增加,直到达到预定的阈值或经历了拥塞。

拥塞控制

当服务器发送 TCP 数据包时,客户端通过返回确认信息(ACKs)来确认收到数据。连接的容量取决于硬件和网络条件。如果服务器发送太多的数据包,速度过快,它们将被丢弃。这意味着没有确认信息,服务器会将其记录为缺失的 ACKs。拥塞控制算法使用发送的数据包和 ACKs 的流量来确定发送速率。

解析

浏览器接收到第一块数据后,就可以开始解析接收到的信息。解析是浏览器将从网络接收到的数据转换为 DOM 和 CSSOM 的步骤,由渲染器用于将页面绘制到屏幕上。DOM 是浏览器的标记语言的内部表示。即使请求的页面 HTML 大于初始的 14KB 包,浏览器也会开始解析并尝试根据它所拥有的数据渲染体验。在任何内容呈现到屏幕之前,必须解析 HTML、CSS 和 JavaScript。

建立 DOM 树

关键渲染路径包括五个步骤。

第一步是处理 HTML 标记并构建 DOM 树。HTML 解析包括 tokenization 和树构造。当解析器发现非阻塞资源时,浏览器将请求这些资源并继续解析。当遇到 CSS 文件时,解析可以继续,但<script>标记,特别是那些没有 async 或 defer 属性的标记,会阻止呈现并暂停 HTML 的解析。过多的脚本可能成为一个重要的瓶颈。

预加载扫描

浏览器在构建 DOM 树时,占用主线程。这时,预加载扫描器会解析可用内容并请求高优先级资源,例如 CSS、JavaScript 和 Web 字体。由于存在预加载扫描器,我们不必等到解析器找到对外部资源的引用才请求它。它会在后台检索资源,以便在主 HTML 解析器到达请求的资源时,它们可能已经在传输过程中,或已经被下载了。预加载扫描器提供的优化可以减少阻塞。

<link rel="stylesheet" href="styles.css" />
<script src="myscript.js" async></script>
<img src="myimage.jpg" alt="image description" />
<script src="anotherscript.js" async></script>

在这个例子中,当主线程解析 HTML 和 CSS 时,预加载扫描器会找到脚本和图像,并开始下载它们。为了确保脚本不会阻塞进程,请添加async属性,或者如果 JavaScript 解析和执行顺序很重要,则添加defer属性。

等待获取 CSS 不会阻塞 HTML 解析或下载,但会阻塞 JavaScript,因为通常使用 JavaScript 来查询 CSS属性对元素的影响。

建立 CSSOM 树

关键渲染路径的第二步是处理 CSS 并构建 CSSOM 树。CSS 对象模型类似于 DOM。

构建 CSSOM 非常快,从性能优化的角度来看,这一步没有什么优化空间,因为创建 CSSOM 的总时间通常少于一个 DNS 查找所需的时间。

渲染

渲染步骤包括样式、布局、绘制和在某些情况下的合成。解析步骤中创建的 CSSOM 和 DOM 树会合并成一个渲染树,然后用于计算每个可见元素的布局,然后将其绘制到屏幕上。在某些情况下,内容可以被提升到自己的图层并进行合成,通过在 GPU 上绘制屏幕的部分而不是 CPU,释放主线程,从而提高性能。

风格

关键渲染路径的第三步是将 DOM 和 CSSOM 合并为一个渲染树。

构建渲染树的过程从 DOM 树的根开始,遍历每个可见节点。

不会被显示的标记,如<head>和其子元素以及任何带有display:none的节点不包括在渲染树中,因为它们不会出现在渲染输出中。带有visibility:hidden属性的节点包括在渲染树中,因为它们占据空间。

布局

关键渲染路径的第四个步骤是在渲染树上运行布局计算每个节点的几何形状。_布局_是决定渲染树中所有节点的宽度、高度和位置的过程,以及页面上每个对象的大小和位置的确定。_回流_是页面或整个文档的任何部分进行的任何后续大小和位置确定。

一旦构建了渲染树,布局就开始了。渲染树确定了哪些节点(即使是不可见的)以及它们的计算样式,但不确定每个节点的尺寸或位置。

在网页上,几乎所有内容都是一个框。不同的设备和桌面首选项意味着有无限数量的不同视口大小。在这个阶段,考虑视口的大小,浏览器确定了所有不同框在屏幕上的尺寸。以视口的大小为基础,布局通常从 body 开始,布置所有 body 后代的尺寸,每个元素的框模型属性提供替换元素的占位空间,它不知道尺寸,比如我们的图片。

第一次确定节点的大小和位置称为_布局_。对节点大小和位置的后续重新计算称为_回流_。在我们的示例中,假设在返回图像之前进行了初始布局。由于我们没有声明图片的大小,因此在了解图像大小后会出现回流。

绘制

关键渲染路径的最后一步是将每个节点绘制到屏幕上,第一次出现的节点称为 First Contentful Paint (首次有意义绘制)。

在绘制或光栅化阶段,浏览器将从网络接收到的每个框转换为 DOM 和 CSSOM,由渲染器用于在屏幕上绘制页面。

DOM 是浏览器标记语言的内部表示。确保平滑滚动和动画,包括计算样式以及回流和重绘在内的占用主线程的所有任务,必须在不到 16.67ms 的时间内完成。

为了确保重绘可以更快地完成,绘制到屏幕通常被分解成几个图层。如果这种情况发生,则需要合成。

将绘制树中的元素分成图层可以提高绘制和重绘性能。将内容推广到 GPU 上的图层(而不是 CPU 上的主线程)可以提高绘制和重绘性能。

有特定的属性和元素会实例化一个图层,包括<video><canvas>,以及具有 CSS 属性的任何元素,例如opacity、三维transformwill-change等等。这些节点将与其后代一起绘制到自己的图层上,除非后代因上述原因之一需要自己的图层。图层确实提高了性能,但在内存管理方面是昂贵的,因此不应在 Web 性能优化策略中过度使用。

合成

当文档的不同部分在不同的层中绘制时,合成是必要的,以确保它们以正确的顺序绘制到屏幕上并正确呈现内容。

随着页面继续加载资源,可能会发生回流。回流会引发重新绘制和重新合成。

如果我们定义了图像的大小,就不需要回流,只会重新绘制需要重新绘制的层,并在必要时进行合成。

当从服务器获取图像时,渲染过程回到布局步骤并从那里重新开始。

Interactivity

当主线程完成页面绘制后,你可能认为我们已经“准备好了”。但这并不一定是这样。如果加载包括 JavaScript,那么必须正确地延迟执行,并且只有在 onload 事件触发后才执行。此时,主线程可能很忙,无法用于滚动、触摸和其他交互。

交互时间(TTI)是从引起 DNS 查找和 SSL 连接的第一次请求到页面交互的测量时间,交互是指在 First Contentful Paint 之后,页面在 50ms 内响应用户交互的时间点。如果主线程正忙于解析、编译和执行 JavaScript,则不可用,因此无法在及时(少于 50ms)响应用户交互。

在我们的示例中,也许图片加载得很快,但是 anotherscript.js 文件的大小为 2MB,我们的用户的网络连接很慢。在这种情况下,用户会非常快速地看到页面,但是在下载、解析和执行脚本之前,无法滚动。这不是一个良好的用户体验。

Untitled

在这个示例中,JavaScript执行需要超过1.5秒,主线程在整个时间内都被完全占用,无法响应点击事件或屏幕点击。